/*
 * XHelpBrowser.java
 * 
 * Copyright (c) 2006 - 2007
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xfc.help;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.JTree;
import javax.swing.SwingConstants;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.xfc.XApp;
import org.xfc.components.XFrame;
import org.xfc.components.XSplitPane;
import org.xfc.dialog.XErrorDialog;
import org.xfc.util.XUtils;
import org.xfc.util.platform.XPlatform;

import com.jgoodies.forms.layout.FormLayout;

// TODO: support zip files w/ html content, and an xml TOC file?
//       doc the supported file types.

// TODO: clean this file up, the API and the memory / resource usage
//       only open help libraries when necessary, close them when finished.

// TODO: have a default .css for html files that don't specify one?

// TODO: use lucerne for text searching

// TODO: follow links

// TODO: sync the contents on the left with the browsing actions


/**
 * 
 * 
 * @author Devon Carew
 */
public class XHelpBrowser
	extends XFrame
{
	static XApp app;
	
	static ImageIcon HELP_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/etool16/help.gif");
	static ImageIcon CONTENTS_OPEN_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/obj16/toc_open.gif");
	static ImageIcon CONTENTS_CLOSED_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/obj16/toc_closed.gif");
	static ImageIcon TOPIC_PARENT_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/obj16/container_obj.gif");
	static ImageIcon TOPIC_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/obj16/topic.gif");
	static ImageIcon SEARCH_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.help.ui/icons/obj16/search_results_view.gif");
	
	static ImageIcon BACK_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.ui/icons/full/elcl16/backward_nav.gif");
	static ImageIcon FORWARD_ICON = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.ui/icons/full/elcl16/forward_nav.gif");
	
	static ImageIcon ERROR_DECORATION = XApp.getApp().getResources().getIcon("/eclipse/org.eclipse.pde.ui/icons/ovr16/error_co.gif");
	
	private Action backAction = new AbstractAction(null, BACK_ICON) {
		public void actionPerformed(ActionEvent event) {
			doBack();
		}
	};
	
	private Action forwardAction = new AbstractAction(null, FORWARD_ICON) {
		public void actionPerformed(ActionEvent event) {
			doForward();
		}
	};
	
	private TitledPanel 			titledPanel;
	private JEditorPane				helpPane;
	
	private JTree 					contentsTree;
	private DefaultTreeModel		treeModel;
	private DefaultMutableTreeNode	rootNode;
	
	private JComponent 				searchResultsComponent;
	
	private BrowserHistory			browserHistory = new BrowserHistory();
	private HelpLocation			currentLocation;
	
	/**
	 * 
	 */
	public XHelpBrowser()
	{
		this(XApp.getApp());
	}

	/**
	 * @param app
	 */
	public XHelpBrowser(XApp app)
	{
		super(app);
		
		setTitle(app.getAppName() + " Help");
		//setIconImage(app.getAppIcon() == null ? HELP_ICON.getImage() : app.getAppIcon().getImage());
		setIconImage(HELP_ICON.getImage());
		
		initUI();
	}
	
	private void initUI()
	{
		JPanel content = new JPanel(new BorderLayout(0, 4));
		
		JToolBar toolbar = XApp.getComponentFactory().createToolBar();
		JTextField textField = new JTextField() {
			public Dimension getPreferredSize() {
				return getMinimumSize();
			}
			public Dimension getMinimumSize() {
				return new Dimension(super.getPreferredSize().width, 18);
			}
		};
		textField.setColumns(14);
		
		toolbar.add(backAction);
		toolbar.add(Box.createHorizontalStrut(2));
		toolbar.add(forwardAction);
		toolbar.add(Box.createHorizontalStrut(1));
		toolbar.addSeparator();
		toolbar.add(Box.createHorizontalStrut(4));
		toolbar.add(new JLabel("Search:"));
		toolbar.add(Box.createHorizontalStrut(4));
		toolbar.add(new WrapperPanel(textField));
		toolbar.add(Box.createHorizontalGlue());
		
		JTabbedPane tabbedPane = new JTabbedPane();
		// TODO: have a way to size the splitpane to the min size of the left or right component
		//       or fix the setDividerLocation() method.
		tabbedPane.setMinimumSize(new Dimension(200, 200));
		tabbedPane.setPreferredSize(new Dimension(200, 200));
		
		tabbedPane.addTab("Contents", CONTENTS_CLOSED_ICON, createContentsComponent());
		tabbedPane.addTab("Search Results", SEARCH_ICON, createSearchResultsComponent());
		
		titledPanel = new TitledPanel();
		titledPanel.setContent(createMainHelpArea());
		
		XSplitPane splitPane = new XSplitPane(XSplitPane.HORIZONTAL);
		
		splitPane.setLeftComponent(tabbedPane);
		splitPane.setRightComponent(titledPanel);
		
		content.add(toolbar, BorderLayout.NORTH);
		content.add(splitPane, BorderLayout.CENTER);
		content.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
		
		getContentPane().setLayout(new BorderLayout());
		getContentPane().add(content, BorderLayout.CENTER);
		
		setInitialSize(new Dimension(800, 700));
		
		updateActions();
	}
	
	private JComponent createContentsComponent()
	{
		rootNode = new DefaultMutableTreeNode();
		treeModel = new DefaultTreeModel(rootNode);
		
		contentsTree = new JTree(treeModel);
		contentsTree.setCellRenderer(new HelpTreeCellRenderer());
		contentsTree.setBorder(BorderFactory.createEmptyBorder(0, 2, 0, 2));
		contentsTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
		contentsTree.setRootVisible(false);
		contentsTree.addTreeSelectionListener(new TreeSelectionListener() {
			public void valueChanged(TreeSelectionEvent event) {
				if (contentsTree.getLastSelectedPathComponent() instanceof TOCItemTreeNode)
				{
					TOCItemTreeNode node = (TOCItemTreeNode)contentsTree.getLastSelectedPathComponent();
					
					navigate(node);
				}
			}
		});
		
		JScrollPane scrollPane = new JScrollPane(contentsTree);
		
		scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		
		//scrollPane.setBorder(null);
		
		return scrollPane;
	}
	
	private JComponent createSearchResultsComponent()
	{
		searchResultsComponent = new JList(new Object[] { "one", "two", "three" });
		
		JScrollPane scrollPane = new JScrollPane(searchResultsComponent);
		
		scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		
		//scrollPane.setBorder(null);
		
		return scrollPane;
	}
	
	private JComponent createMainHelpArea()
	{
		helpPane = new JEditorPane();
		helpPane.setEditable(false);
		//helpPane.setContentType("text/html");
		helpPane.addHyperlinkListener(new HyperlinkListener() {
			public void hyperlinkUpdate(HyperlinkEvent event) {
				if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
					navigate(event.getURL());
			}
		});
		
		JScrollPane scrollPane = new JScrollPane(helpPane);
		
		scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
		
		scrollPane.setPreferredSize(new Dimension(400, 200));
		scrollPane.setMinimumSize(new Dimension(400, 200));
		
		//scrollPane.setBorder(null);
		
		return scrollPane;
	}
	
	private void navigate(TOCItemTreeNode node)
	{
		if (node == null || node.tocItem == null)
			return;
		
		XContentsItem item = node.tocItem;
		
		XTableOfContents parentTOC = node.getParentXContents();
		
		if (parentTOC != null)
			titledPanel.setTitleText(parentTOC.getTitle());
		
		if (item.getLocation() == null)
			return;
		
		HelpLocation helpLocation = new HelpLocation(node.getHelpLibrary(), parentTOC, item.getLocation());
		
		navigate(helpLocation, true);
	}
	
	private void navigate(URL url)
	{
		if (!"localhost".equals(url.getHost()))
		{
			try
			{
				// Use the system's browser.
				XPlatform.showInBrowser(url);
			}
			catch (IOException ioe)
			{
				XErrorDialog.showError("Error Launching Browser", ioe);
			}
		}
		else
		{
			HelpLocation helpLocation = new HelpLocation(currentLocation.helpLibrary, currentLocation.toc, url.getPath());
			
			navigate(helpLocation, true);
		}
	}
	
	private void navigate(HelpLocation helpLocation, boolean updateHistory)
	{
		titledPanel.setTitleText(helpLocation.toc.getTitle());
		
		try
		{
			URL url = helpLocation.helpLibrary.getContentAsURL(helpLocation.location);
			
			helpPane.setPage(url);
			
			if (updateHistory)
				browserHistory.browseTo(helpLocation);
			
			currentLocation = helpLocation;
			
			updateActions();
			
			helpPane.scrollRectToVisible(new Rectangle(0, 0, 100, 100));
		}
		catch (IOException ioe)
		{
			ioe.printStackTrace();
		}
	}
	
	public void addJavaHelpLibrary(File helpFile)
		throws IOException
	{
		boolean hadNoNodes = rootNode.getChildCount() == 0;
		
		XHelpLibrary helpLibrary = new XJavaHelpLibrary(helpFile);
		
		XTableOfContents toc = helpLibrary.getTableOfContents();
		
		TOCItemTreeNode newNode = new TOCItemTreeNode(null, toc);
		
		newNode.setUserObject(helpLibrary);
		
		treeModel.insertNodeInto(newNode, rootNode, rootNode.getChildCount());
		
		if (hadNoNodes)
			contentsTree.expandPath(new TreePath(rootNode));
	}
	
	public void addEclipseLibrary(File helpFile)
		throws IOException
	{
		boolean hadNoNodes = rootNode.getChildCount() == 0;
		
		XHelpLibrary helpLibrary = new XEclipseHelpLibrary(helpFile);
		
		XTableOfContents toc = helpLibrary.getTableOfContents();
		
		TOCItemTreeNode newNode = new TOCItemTreeNode(null, toc);
		
		newNode.setUserObject(helpLibrary);
		
		treeModel.insertNodeInto(newNode, rootNode, rootNode.getChildCount());
		
		if (hadNoNodes)
			contentsTree.expandPath(new TreePath(rootNode));
	}
	
	private void doBack()
	{
		HelpLocation location = (HelpLocation)browserHistory.doBack();
		
		backAction.setEnabled(false);
		
		navigate(location, false);
		
		updateActions();
	}
	
	private void doForward()
	{
		HelpLocation location = (HelpLocation)browserHistory.doForward();
		
		forwardAction.setEnabled(false);
		
		navigate(location, false);
		
		updateActions();
	}
	
	private void updateActions()
	{
		backAction.setEnabled(browserHistory.isBackEnabled());
		forwardAction.setEnabled(browserHistory.isForwardEnabled());
	}
	
	
	class HelpLocation
	{
		XHelpLibrary 		helpLibrary;
		XTableOfContents 	toc;
		String 				location;
		
		HelpLocation(XHelpLibrary helpLibrary, XTableOfContents toc, String location)
		{
			this.helpLibrary = helpLibrary;
			this.toc = toc;
			this.location = location;
		}
	}
}

class WrapperPanel
	extends JPanel
{
	
	public WrapperPanel(JTextField textField)
	{
		setLayout(new FormLayout("pref", "pref:grow"));
		
		add(textField, "1, 1, l, c");
	}
	
}

class TitledPanel
	extends JPanel
{
	private JLabel 		label;
	private JComponent	content;
	
	public TitledPanel()
	{
		setLayout(new BorderLayout());
		
		label = new JLabel();
		label.setFont(label.getFont().deriveFont(Font.BOLD));
		label.setBorder(BorderFactory.createEmptyBorder(3, 7, 4, 4));
//		label.setBorder(BorderFactory.createCompoundBorder(
//			BorderFactory.createMatteBorder(0, 0, 0, 1, Color.GRAY),
//			BorderFactory.createEmptyBorder(5, 8, 4, 2)));
//		label.setBorder(BorderFactory.createCompoundBorder(
//			BorderFactory.createRaisedBevelBorder(),
//			BorderFactory.createEmptyBorder(3, 8, 3, 4)));
		
		add(label, BorderLayout.NORTH);
		//setBorder(BorderFactory.createLineBorder(Color.GRAY));
		setBorder(BorderFactory.createRaisedBevelBorder());
		
		setTitleText("");
	}
	
	public JComponent getContent()
	{
		return content;
	}
	
	public void setContent(JComponent content)
	{
		if (this.content != null)
			remove(this.content);
		
		this.content = content;
		
		if (this.content != null)
			add(this.content, BorderLayout.CENTER);
	}
	
	public String getTitleText()
	{
		return label.getText();
	}
	
	public void setTitleText(String titleText)
	{
		if (titleText == null || titleText.length() == 0)
			label.setText(" ");
		else
			label.setText(titleText);
	}
}

class TOCItemTreeNode
	implements MutableTreeNode
{
	TreeNode	parent;
	
	XContentsItem	tocItem;
	
	List nodes = new ArrayList();
	
	TOCItemTreeNode(List roots)
	{
		for (Iterator itor = roots.iterator(); itor.hasNext(); )
		{
			XContentsItem tocItem = (XContentsItem)itor.next();
			
			nodes.add(new TOCItemTreeNode(this, tocItem));
		}
	}
	
	TOCItemTreeNode(TreeNode parent, XContentsItem tocItem)
	{
		this.parent = parent;
		this.tocItem = tocItem;
		
		for (Iterator itor = tocItem.getChildren().iterator(); itor.hasNext(); )
		{
			XContentsItem childItem = (XContentsItem)itor.next();
			
			nodes.add(new TOCItemTreeNode(this, childItem));
		}
	}
	
	public Enumeration children()
	{
		return null;
	}

	public boolean getAllowsChildren()
	{
		return true;
	}

	public TreeNode getChildAt(int childIndex)
	{
		return (TOCItemTreeNode)nodes.get(childIndex);
	}

	public int getChildCount()
	{
		return nodes.size();
	}

	public int getIndex(TreeNode node)
	{
		return nodes.indexOf(node);
	}

	public TreeNode getParent()
	{
		return parent;
	}

	public boolean isLeaf()
	{
		return getChildCount() == 0;
	}
	
	public String toString()
	{
		if (tocItem == null)
		{
			return "TOC";
		}
		else
		{
			return tocItem.toString();
		}
	}

	// for the mutable interface
	private Object userObject;
	
	public void insert(MutableTreeNode arg0, int arg1)
	{
	}

	public void remove(int arg0)
	{
	}

	public void remove(MutableTreeNode arg0)
	{
	}

	public void removeFromParent()
	{
	}

	public void setParent(MutableTreeNode arg0)
	{
	}

	public void setUserObject(Object object)
	{
		this.userObject = object;
	}
	
	public XHelpLibrary getHelpLibrary()
	{
		if (userObject != null)
			return (XHelpLibrary)userObject;
		
		if (!(getParent() instanceof TOCItemTreeNode))
			return null;
		
		return ((TOCItemTreeNode)getParent()).getHelpLibrary();
	}
	
	public XTableOfContents getParentXContents()
	{
		if (tocItem instanceof XTableOfContents)
			return (XTableOfContents)tocItem;
		
		if (!(getParent() instanceof TOCItemTreeNode))
			return null;
		
		return ((TOCItemTreeNode)getParent()).getParentXContents();
	}
}

class HelpTreeCellRenderer
	extends DefaultTreeCellRenderer
{
	HelpTreeCellRenderer()
	{
		
	}

	public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus)
	{
		JLabel label = (JLabel)super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
		
		if (value instanceof TOCItemTreeNode)
		{
			TOCItemTreeNode node = (TOCItemTreeNode)value;
			XContentsItem tocItem = node.tocItem;
			
			// TODO: have different icons if the topic has content?
			
			if (tocItem == null)
			{
				label.setIcon(expanded ? XHelpBrowser.CONTENTS_OPEN_ICON : XHelpBrowser.CONTENTS_CLOSED_ICON);
			}
			else if (tocItem instanceof XTableOfContents)
			{
				label.setIcon(expanded ? XHelpBrowser.CONTENTS_OPEN_ICON : XHelpBrowser.CONTENTS_CLOSED_ICON);
				
				if (((XTableOfContents)tocItem).hasError())
				{
					label.setIcon(XUtils.decorateIcon(label.getIcon(), XHelpBrowser.ERROR_DECORATION,
						SwingConstants.LEFT, SwingConstants.BOTTOM));
				}
			}
			else
			{
				label.setIcon(tocItem.hasChildren() ? XHelpBrowser.TOPIC_PARENT_ICON : XHelpBrowser.TOPIC_ICON);
			}
		}
		
		return label;
	}
	
}

class BrowserHistory
{
	private Stack 	history = new Stack();
	private int		pos;
	
	public BrowserHistory()
	{
		
	}
	
	public void browseTo(Object item)
	{
		while (pos < history.size())
			history.pop();
		
		history.push(item);
		pos++;
	}
	
	public Object doBack()
	{
		pos--;
		
		return history.get(pos - 1);
	}
	
	public Object doForward()
	{
		pos++;
		
		return history.get(pos - 1);
	}
	
	public boolean isBackEnabled()
	{
		return pos > 1;
	}
	
	public boolean isForwardEnabled()
	{
		return pos < history.size();
	}
	
}
